package cn.sh.tools.util.file;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.ClassPathResource;

import java.io.*;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
 * @author shihao
 * @description 文件工具
 */
@Slf4j
public class FileUtils extends FileUtil {

    public static String normalized(String path) {
        if (!path.endsWith(File.separator)) {
            path = path + File.separator;
        }
        return normalize(path);
    }

    /**
     * 读取【类路径（classpath）】上的文件内容，区别于读取【文件系统】上的文件。
     * 当 spring boot项目 打成 可执行的jar包后，classpath资源被封装在jar文件中，不代表一个物理的文件系统路径。
     * 所以，应该使用类似于 InputStream 这样的抽象来处理这些资源，而不是尝试解析物理路径（不能直接使用File读取）。
     *
     * <pre>
     *  1- Classpath（类路径）
     *     指的是 jvm 或者 java编译器 用来查找 java类文件的一个参数。类路径 会告诉 jvm和编译器 去哪里查找用户定义的类和依赖的第三方库（jar包）。
     *  2- Spring Boot 中的 Classpath（类路径）
     *      指的是 Java虚拟机 查找【类文件 和 资源文件】的路径集合。Classpath 可以包括 jar文件、war文件、资源文件。
     *      在 spring boot 项目中，src/main/java（存放java源代码）、src/main/resources（存放资源文件，比如，application.yml等） 两个目录
     *    在构建的过程中会被自动加入到 classpath 中。
     *      在 spring boot 项目中，通常都会将其构建成一个可执行的jar包，这个jar包中 通常有一个 /BOOT-INF/classes目录 用来存放
     *    应用程序本身的类文件和资源文件，以及一个 /BOOT-INF/lib目录 用来存放依赖的第三方jar包。
     * </pre>
     *
     * @return 文件内容
     */
    public static List<String> readFileFromClasspath(String fileFullName, Charset charset) {
        Assert.notBlank(fileFullName, "文件名[fileFullName]必须输入");
        // 通过 ClassPathResource 来访问 classpath（类路径）下的文件
        ClassPathResource classPathResource = new ClassPathResource(fileFullName);

        List<String> contentList;
        try (
                InputStream inputStream = classPathResource.getInputStream();
                InputStreamReader isr = new InputStreamReader(inputStream, charset);
                BufferedReader reader = IOUtils.toBufferedReader(isr);
        ) {
            // contentList = IOUtils.readLines(inputStream, StandardCharsets.UTF_8.name());
            /**
             * ClassPathResource 通过方法 getFile() 获取File只适用于指向文件系统中的资源。
             * 如果 spring boot 项目打包成 jar包（相应的资源也会被打包到 jar包中），则调用 getFile()会抛出异常。
             * 在这种情况下，需要使用 InputStream来读取资源
             */
            contentList = new ArrayList<>();
            String line = reader.readLine();
            while (StrUtil.isNotBlank(line)) {
                contentList.add(line);
                line = reader.readLine();
            }

            if (contentList.isEmpty()) {
                throw new RuntimeException(StrUtil.format("文件[{}]内容不能为空", fileFullName));
            }

            return contentList;
        } catch (IOException e) {
            log.error(StrUtil.format("读取[{}]文件异常", fileFullName), e);
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 读取文件内容，推荐读取大文件时使用该方法。
     *
     * @param filePath 文件路径 + 文件名
     * @param charset  文件编码，默认使用 StandardCharsets.UTF_8
     * @throws FileNotFoundException 如果文件不存在
     * @throws IOException           如果读取文件发生错误
     */
    public static StringBuilder readFileOfByteBuffer(String filePath, Charset charset)
            throws Exception {
        try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
             FileChannel fileChannel = file.getChannel()) {

            /**
             * 1）. 直接内存使用
             *  ① allocate()方法：
             *      这个方法会分配一个新的 非直接缓冲区。
             *      非直接缓冲区的内容位于 Java堆内存中，这意味着它受到垃圾回收器的影响。
             *  ② allocateDirect()方法：
             *      这个方法会分配一个新的 直接缓冲区。
             *      直接缓冲区的内容位于 操作系统的本地内存中（具体的来说，应该是在 java8内存结构中本地内存中的直接内存），
             *    而不是 Java堆内存中，所以它不受垃圾回收的影响。
             *      直接缓冲区可以提供更高的 I/O性能，因为对于本地 I/O操作而言，它不需要额外的内存复制操作（Java堆 和 本地内存 之间的数据复制）。
             * 2）. 缓冲区预分配
             *  ① ByteBuffer 的容量最好不要是固定值。因为，在处理大文件的时候，可能需要多个这样的缓冲区来装载整个文件的内容。
             *  ② 使用 StringBuilder 累加文件内容是很高效的，比 String 拼接在循环冲更加高效。
             *      但是，在处理大文件的时候，capacity 的大小可能需要动态调整，或者根据文件的实际大小预先分配足够的空间，避免多次扩容带来的开销。
             *  综述，对于大文件，提前知道文件大小并且预分配 ByteBuffer和StringBuilder 的大小能显著提高性能，减少内存重新分配的次数。
             */
            // 1- 创建一个 ByteBuffer（字节缓冲区），用于存放读取到的数据
            // 获取文件大小
            long fileSize = file.length();
            // 根据文件大小动态分配 ByteBuffer 的容量
            int capacity = ((int) Math.min(fileSize, Integer.MAX_VALUE));
            // 方式一：使用【非直接】字节缓冲区
            //ByteBuffer buffer = ByteBuffer.allocate(capacity);
            // 方式二：使用【直接】字节缓冲区
            ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);

            // 2- 创建一个 utf8 的 CharsetDecoder
            if (null == charset) {
                charset = StandardCharsets.UTF_8;
            }
            CharsetDecoder decoder = charset.newDecoder();

            // 3- 读取数据到 buffer
            // 将读取到的内容存放到 StringBuilder
            StringBuilder content = new StringBuilder((int) fileSize);
            while (fileChannel.read(buffer) != -1) {
                // 切换到读模式，准备读取数据
                buffer.flip();
                // 使用 指定字符集 解码 ByteBuffer 到 CharBuffer，再转换为 String
                content.append(decoder.decode(buffer));
                // 清空缓冲区，准备下一次读取
                buffer.clear();
            }

            /**
             * 显式的释放 直接缓冲区占用的堆外内存。
             *
             * 通过ByteBuffer.allocateDirect()分配的直接缓冲区（Direct Buffer）使用的是JVM堆外内存。
             * 与传统的堆内存不同，直接缓冲区不受Java垃圾回收器（GC）的直接管理，但是它们仍然会被间接地管理。
             *
             * 直接缓冲区 与 一个Java对象关联（即ByteBuffer对象），该对象处于JVM的管理之下。
             * 当这个Java对象变得不可达时（即没有任何引用指向它），垃圾回收器可以对其进行垃圾回收。但是，这种方式并不是立即释放内存，而是要等到垃圾收集器运行时才会进行。
             * 在垃圾回收过程中，可以通过对象的清理（finalization）机制或者Java 9引入的Cleaner类来释放直接缓冲区占用的堆外内存。
             *
             * 依赖于垃圾回收来回收直接缓冲区的内存可能会存在延迟，因为垃圾回收器无法直接感知堆外内存的压力。
             * 因此，如果快速和频繁地分配大量的直接缓冲区，可能会导致内存耗尽，特别是在有限的堆外内存资源的情况下。
             * 为了更好地管理直接缓冲区的堆外内存，可以使用 Cleaner 类显示的释放直接缓冲区占用的堆外内存。
             */
            cleanDirectMemory(buffer);

            // 返回读取到的文件内容
            return content;
        } catch (FileNotFoundException e) {
            // 抛出文件不存在的异常，便于调用者识别和处理文件不存在的情况。
            throw new FileNotFoundException("文件不存在: " + filePath);
        } catch (IOException e) {
            // 抛出IO异常，便于调用者识别和处理IO异常。
            throw new IOException("读取文件失败: " + filePath, e);
        }
    }

    private static final Method cleanerMethod;
    private static final Method cleanMethod;

    static {
        // java8及之前 通过反射获取 cleaner() 相关方法
        try {
            cleanerMethod = Class.forName("java.nio.DirectByteBuffer").getMethod("cleaner");
            cleanerMethod.setAccessible(true);
            cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean");
            cleanMethod.setAccessible(true);
        } catch (NoSuchMethodException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 显式释放直接缓冲区占用的内存
     *
     * @param byteBuffer 字节缓冲区（这里指的是 直接缓冲区，因为 非直接缓冲区 在 Java堆内存上，GC可以进行自动内存管理）
     */
    private static void cleanDirectMemory(ByteBuffer byteBuffer) throws Exception {
        if (null == byteBuffer || !byteBuffer.isDirect()) {
            return;
        }

        // 下面是 jdk8及之前的写法。从 Java9开始，推荐的释放直接缓冲区内存的方式是使用 sun.misc.Unsafe 类或者使用 java.lang.ref.Cleaner 类。
        try {
            // 获取 Cleaner
            Object cleaner = cleanerMethod.invoke(byteBuffer);
            // 调用 Cleaner 的 clean()方法 来显示的释放 直接缓冲区占用的 堆外内存（这里指的是 本地内存中的直接内存）。
            if (null != cleaner) {
                cleanMethod.invoke(cleaner);
            }
        } catch (Exception e) {
            throw new Exception("释放接缓冲区占用的堆外内存失败", e);
        }
    }

    /**
     * 逐行读取文件内容。这种方式更适合处理大文件，因为它不会一次性将文件的所有内容加载到内存中。
     *
     * @param filePath 文件路径 + 文件名
     * @param charset  文件编码，默认使用 StandardCharsets.UTF_8
     */
    private static List<String> readLine(String filePath, Charset charset) throws IOException {
        // 逐行读取文件内容，这里的代码逻辑直接抄袭 Files.readAllLines()方法就可以。
        List<String> contentList = new ArrayList<>();
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filePath), charset)) {
            for (; ; ) {
                // 逐行读取
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                contentList.add(line);
            }
        }
        return contentList;
    }

    /**
     * 一次性读取文件的所有行到一个List<String>中。这种方式适合于读取较小的文件，因为它会将文件的所有内容一次性加载到内存中。
     *
     * @param filePath 文件路径 + 文件名
     * @param charset  文件编码，默认使用 StandardCharsets.UTF_8
     */
    private static List<String> readAllLines(String filePath, Charset charset) throws IOException {
        // 一次性读取文件的所有行到一个List<String>中。
        return Files.readAllLines(Paths.get(filePath), charset);
    }

}
